﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Threading.Tasks;
using System.Windows;
using Red.Core;
using Red.Launcher.Utilities;

namespace Red.Launcher.ViewModels.Workspaces.Database
{
    public class Metadata
    {
        #region Private Data

        private string _foreignKey;

        #endregion Private Data

        #region Public Delegates

        public delegate Record CreateDefaultFunc();
        public delegate Record CreateFromDatabaseRowFunc( Dictionary<string, object> rowData );
        public delegate Sql.Select GenerateSelectSqlFunc( Record parent );

        #endregion Public Delegates

        #region Public Properties

        public string Table { get; set; }
        public bool CanCopy { get; set; }

        public IEnumerable<string> Keys { get; set; }

        /// <summary>
        /// Valid types of records that can be set as children to this record type
        /// </summary>
        public List<Type> Children { get; private set; } = new List<Type>();

        /// <summary>
        /// Records that can be sql-joined to this one (usually they represent optional or non-critical information)
        /// </summary>
        public List<Type> Joins { get; private set; } = new List<Type>();

        /// <summary>
        /// Types of records that can be assigned and then reinterpreted into other types (but not be direct children)
        /// </summary>
        public List<Type> Assignable { get; private set; } = new List<Type>();

        public CreateDefaultFunc CreateDefault { get; set; }
        public CreateFromDatabaseRowFunc CreateFromDatabaseRow { get; set; }
        public GenerateSelectSqlFunc GenerateSelectSql { get; set; }

        public string ForeignKey
        {
            get
            {
                if ( string.IsNullOrEmpty( _foreignKey ) )
                {
                    if ( string.IsNullOrEmpty( Table ) )
                    {
                        return null;
                    }

                    return Table.ToLower() + "_id";
                }
                return _foreignKey;
            }
            set
            {
                if ( _foreignKey != value )
                {
                    _foreignKey = value;
                }
            }
        }

        #endregion Public Properties

        #region C-Tor

        public Metadata()
        {
            GenerateSelectSql = GenerateSelectSqlStandard;
        }

        #endregion C-Tor

        #region Public Methods

        public Record Create()
        {
            return CreateDefault();
        }

        public Record Create( Dictionary<string, object> rowData )
        {
            return CreateFromDatabaseRow( rowData );
        }

        public Sql.Select GenerateSelectSqlStandard( Record parent )
        {
            Sql.Select.Table source = Sql.Select.CreateTable( Table, "std" );

            Sql.Select query = new Sql.Select();
            query.Sources.Add( source );
            query.AddColumn( "*", source );

            if ( !parent.IsVirtual )
            {
                query.Conditions.AddLiteral( parent.Metadata.ForeignKey, parent.Id, source );
            }

            return query;
        }

        #endregion Public Methods
    }

    public interface IRecordWrapper
    {
        Record ActualRecord { get; }

        Metadata ActualMetadata { get; }
    }

    public abstract class Record : ViewModel
    {
        #region Delegates

        protected internal delegate void FetchComplete();
        public delegate void RecordEvent( Record affectedRecord );

        #endregion Delegates

        #region Private Data

        private static int _idPool = 0;
        private int _id = --_idPool;

        private string _name;
        private bool _isWatchingForChanges = false;

        private ObservableCollection<Record> _children = new ObservableCollection<Record>();
        private List< Record > _created = new List<Record>();
        private List< Record > _deleted = new List<Record>();
        private List< Record > _modified = new List<Record>();

        #endregion Private Data

        #region Public Properties

        public bool IsNewlyCreated { get { return Id <= 0; } }
        public ICollection<Record> Deleted { get { return _deleted; } }

        public ObservableCollection<Record> Children { get { return _children; } }

        public int Id { get { return _id; } set { _id = value; } }

        public string Name
        {
            get { return _name; }
            set
            {
                if ( _name != value )
                {
                    _name = value;
                    OnPropertyChanged();
                }
            }
        }

        public bool IsFetchInProgress { get; private set; }

        public bool IsVirtual { get; set; }

        public bool HasNonVirtualChildren
        {
            get
            {
                foreach ( Record child in Children )
                {
                    if ( !child.IsVirtual )
                    {
                        return true;
                    }
                }

                return _deleted.Count > 0;
            }
        }

        #endregion Public Properties

        #region Protected Properties

        protected bool IsWatchingForChanges
        {
            get { return _isWatchingForChanges; }
            set
            {
                if ( value != _isWatchingForChanges )
                {
                    if ( value == true )
                    {
                        PropertyChanged += Base_PropertyChanged;
                        _children.CollectionChanged += Children_CollectionChanged;
                    }
                    else
                    {
                        PropertyChanged -= Base_PropertyChanged;
                        _children.CollectionChanged -= Children_CollectionChanged;
                    }

                    _isWatchingForChanges = value;
                }
            }
        }

        #endregion Protected Properties

        #region Interface Properties

        public abstract string Icon { get; }
        public abstract IEnumerable<object> Values { get; }
        public abstract Metadata Metadata { get; }

        #endregion Interface Properties

        #region C-Tor

        public Dictionary<Type, Dictionary<int, Record>> Records { get; protected set; }

        public Record()
        {
        }

        #endregion C-Tor

        #region Internal Methods

        // Methods that can only be accessed by this assembly

        internal Record FindRecord( Type type, int id )
        {
            if ( Records != null )
            {
                Dictionary<int, Record> typeMap;

                if ( Records.TryGetValue( type, out typeMap ) )
                {
                    Record record;
                    if ( typeMap.TryGetValue( id, out record ) )
                    {
                        return record;
                    }
                }
            }

            return null;
        }

        internal Record FindChild( string name )
        {
            foreach ( var child in Children )
            {
                if ( child.Name == name )
                {
                    return child;
                }
            }

            return null;
        }

        internal void AddToRecords( ref Record record )
        {
            if ( Records != null )
            {
                Type type = record.GetType();

                Dictionary<int, Record> typeMap;

                if ( !Records.TryGetValue( type, out typeMap ) )
                {
                    typeMap = new Dictionary<int, Record>();
                    Records.Add( type, typeMap );
                }

                Record existing;
                if ( typeMap.TryGetValue( record.Id, out existing ) )
                {
                    record = existing;
                }
                else
                {
                    typeMap.Add( record.Id, record );
                }

                record.Records = Records;
            }
        }

        internal void AddNewChild()
        {
            Metadata childMetadata = (Metadata)Metadata.Children[ 0 ].GetProperty( "METADATA" ).GetValue( null, null );
            Record child = childMetadata.Create();
            if ( child != null )
            {
                child.IsWatchingForChanges = true;
                AddChild( ref child );
            }
        }

        internal void AddChild( ref Record child )
        {
            AddToRecords( ref child );

            _children.Add( child );
        }

        #region Drag and Drop

        internal class DraggedRecord
        {
            public Record Record { get; set; }
            public Record Parent { get; set; }
        }

        /// <summary>
        /// Check to see if this record is a valid recipient for the data associated with a drag and drop operation
        /// </summary>
        /// <remarks>
        /// If the data contained within the IDataObject is a <see cref="Record"/>, it will be associated with the derived record type, but the actual contents will be a <see cref="DraggedRecord"/>
        /// </remarks>
        /// <param name="data">Data object associated with the drag operation</param>
        /// <returns><see langword="true"/> if this <see cref="Record"/> is a valid drop target</returns>
        internal virtual bool AcceptsDraggedData( IDataObject data )
        {
            return false;
        }

        /// <summary>
        /// Complete drag and drop operation with this Record as the target to receive data
        /// </summary>
        /// <remarks><see cref="AcceptsDraggedData"/> must have been called prior to this function, and must return <see langword="true"/></remarks>
        /// <param name="data">Data object associated with the drag operation</param>
        internal virtual void PerformDrop( IDataObject data ) { }

        #endregion Drag and Drop

        #region Database Interaction

        /// <summary>
        /// Send all changes to the database
        /// </summary>
        internal async Task Commit( DatabaseHelper.ExecuteCompleteCallback completeCallback = null )
        {
            Sql.Transaction transaction = new Sql.Transaction();
            GenerateInsertSql( ref transaction );
            GenerateUpdateSql( ref transaction );
            GenerateDeleteSql( ref transaction );

            await Commit( transaction, completeCallback );
        }

        /// <summary>
        /// Retrieve entries from the database
        /// </summary>
        /// <param name="dispatcher">Required so that callbacks can be performed on the UI thread</param>
        /// <param name="callback">Called when the fetch operation has been completed</param>
        internal async Task Fetch( FetchComplete callback = null )
        {
            if ( IsFetchInProgress )
                return;

            if ( Metadata.Children.Count == 0 )
            {
                callback?.Invoke();

                return;
            }

            IsFetchInProgress = true;
            int fetchesCompleted = 0;

            Task[] fetchTasks = new Task[ Metadata.Children.Count ];
            int currentFetchIndex = 0;

            foreach ( Type childType in Metadata.Children )
            {
                Metadata childMetadata = (Metadata)childType.GetProperty( "METADATA" ).GetValue( null, null );

                Sql.ISql query = childMetadata.GenerateSelectSql( this );

                if ( query != null )
                {
                    string sql = query.Sql;

                    fetchTasks[ currentFetchIndex++ ] = DatabaseHelper.FetchAsync
                    (
                        sql,
                        Settings.DatabaseConnectionString,
                        ( data ) =>
                        {
                            if ( !IsFetchInProgress )
                            {
                                throw new InvalidOperationException( string.Format( "Database fetch complete callback called before all item callbacks were called\nThis probably means there's a bug in the fetch somewhere\nOffending SQL:\n{0}", sql ) );
                            }

                            Record record = childMetadata.CreateFromDatabaseRow( data );

                            // If the child is a type of record that wraps another record, then
                            // we should be able to construct the wrapped type from the same query
                            IRecordWrapper wrapper = record as IRecordWrapper;
                            if ( wrapper != null && wrapper.ActualRecord == null )
                            {
                                Record wrapped = wrapper.ActualMetadata.CreateFromDatabaseRow( data );

                                // Only add to records. Wrapper types will use the Records dictionary to provide
                                // the wrapped record and the wrapped record is not a child this one
                                AddToRecords( ref wrapped );

                                wrapped.IsWatchingForChanges = true;
                            }

                            AddChild( ref record );
                        },
                        ( count, error ) =>
                        {
                            foreach ( Record child in Children )
                            {
                                child.IsWatchingForChanges = true;
                            }

                            ++fetchesCompleted;

                            if ( fetchesCompleted == Metadata.Children.Count )
                            {
                                IsFetchInProgress = false;

                                callback?.Invoke();
                            }
                        }
                    );
                }
                else
                {
                    fetchTasks[ currentFetchIndex++ ] = Task.FromResult( 0 );
                    ++fetchesCompleted;

                    if ( fetchesCompleted == Metadata.Children.Count )
                    {
                        IsFetchInProgress = false;

                        callback?.Invoke();
                    }
                }
            }

            await Task.WhenAll( fetchTasks );
        }

        /// <summary>
        /// Only fetches if it hasn't been done before
        /// Guaranteed to have completed once the caller has finished awaiting
        /// </summary>
        /// <returns></returns>
        internal async Task FetchOnceSafe()
        {
            if ( !HasNonVirtualChildren )
            {
                TaskCompletionSource<bool> tcs = new TaskCompletionSource<bool>();

                FetchComplete callback = () => tcs.SetResult( true );
                await Fetch( callback );
                await tcs.Task;
            }
        }

        #endregion Database Interaction

        #endregion Internal Methods

        #region Private Methods

        #region Database Interaction

        private async Task Commit( Sql.Transaction transaction, DatabaseHelper.ExecuteCompleteCallback completeCallback )
        {
            if ( !transaction.IsEmpty )
            {
                await DatabaseHelper.ExecuteAsync( transaction.Sql, Settings.DatabaseConnectionString, transaction.Parameters, completeCallback );
            }
            else
            {
                completeCallback?.Invoke( DatabaseHelper.Result.Success );
            }
        }

        #endregion Database Interaction

        #region SQL

        protected virtual void GenerateInsertSql( ref Sql.Transaction transaction, Record parent )
        {
            Sql.Insert query = new Sql.Insert( this );

            if ( !parent.IsVirtual )
            {
                query.Parents.Add( parent );
            }

            transaction.Add( query );
        }

        protected virtual void GenerateUpdateSql( ref Sql.Transaction transaction, Record parent )
        {
            Sql.Update query = new Sql.Update( this );

            transaction.Add( query );
        }

        private void GenerateInsertSql( ref Sql.Transaction transaction )
        {
            IRecordWrapper wrapper = this as IRecordWrapper;
            if( wrapper != null )
            {
                wrapper.ActualRecord.GenerateInsertSql( ref transaction );
            }

            foreach ( Record record in _created )
            {
                record.GenerateInsertSql( ref transaction, this );
            }

            foreach ( Record child in Children )
            {
                child.GenerateInsertSql( ref transaction );
            }
        }

        private void GenerateUpdateSql( ref Sql.Transaction transaction )
        {
            if ( IsNewlyCreated && !IsVirtual )
            {
                return;
            }

            IRecordWrapper wrapper = this as IRecordWrapper;
            if ( wrapper != null )
            {
                wrapper.ActualRecord.GenerateUpdateSql( ref transaction );
            }

            foreach ( Record child in Children )
            {
                child.GenerateUpdateSql( ref transaction );
            }

            foreach ( Record updated in _modified )
            {
                updated.GenerateUpdateSql( ref transaction, this );
            }
        }

        private void GenerateDeleteSql( ref Sql.Transaction transaction )
        {
            if ( IsNewlyCreated && !IsVirtual )
            {
                return;
            }

            IRecordWrapper wrapper = this as IRecordWrapper;
            if ( wrapper != null )
            {
                wrapper.ActualRecord.GenerateDeleteSql( ref transaction );
            }

            foreach ( Record child in Children )
            {
                child.GenerateDeleteSql( ref transaction );
            }

            // Remove all descendants from deleted children (since they've now been "orphaned")
            foreach ( Record child in _deleted )
            {
                child.GenerateDeleteSql( ref transaction, this );
            }
        }

        /// <summary>
        /// Deletes all children and descendants of this database record
        /// </summary>
        /// <param name="transaction">The collection of sql queries that is being constructed and will be executed as a single transaction</param>
        /// <param name="parent">The record that parents this one, and grandparents it's children</param>
        protected virtual void GenerateDeleteSql( ref Sql.Transaction transaction, Record parent )
        {
            Sql.Delete query = new Sql.Delete( this );
            transaction.Add( query );

            // Recurse through the tree and delete all descendants
            foreach ( Record child in Children )
            {
                if ( !child.IsNewlyCreated )
                {
                    child.GenerateDeleteSql( ref transaction, this );
                }
            }
        }

        #endregion SQL

        #endregion Private Methods

        #region Events

        public event RecordEvent OnChildDeleted;
        public event RecordEvent OnChildCollectionChanged;

        #endregion Events

        #region Event Handlers

        private void Base_PropertyChanged( object sender, PropertyChangedEventArgs e )
        {
            if ( _created.IndexOf( this ) == -1 && _modified.IndexOf( this ) == -1 )
            {
                _modified.Add( this );
            }

            System.Diagnostics.Debug.Print( "Record changed: ({0}) {1}", Id, Name );
            System.Diagnostics.Debug.Print( "Property Changed: " + e.PropertyName );
            System.Diagnostics.Debug.Print( "Created items: " + _created.Count );
            System.Diagnostics.Debug.Print( "Deleted items: " + _deleted.Count );
            System.Diagnostics.Debug.Print( "Modified items: " + _modified.Count );
        }

        private void Children_CollectionChanged( object sender, NotifyCollectionChangedEventArgs e )
        {
            if ( !IsFetchInProgress )
            {
                if ( e.Action == NotifyCollectionChangedAction.Add )
                {
                    foreach ( Record item in e.NewItems )
                    {
                        if ( !item.IsVirtual )
                        {
                            _created.Add( item );
                        }
                    }
                }
                else if ( e.Action == NotifyCollectionChangedAction.Remove )
                {
                    foreach ( Record item in e.OldItems )
                    {
                        int index = _created.IndexOf( item );

                        if ( index == -1 )
                        {
                            _deleted.Add( item );
                        }
                        else
                        {
                            _created.Remove( item );
                        }

                        OnChildDeleted?.Invoke( item );
                    }
                }

                System.Diagnostics.Debug.Print( "Record changed: ({0}) {1}", Id, Name );
                System.Diagnostics.Debug.Print( "Created items: " + _created.Count );
                System.Diagnostics.Debug.Print( "Deleted items: " + _deleted.Count );
                System.Diagnostics.Debug.Print( "Modified items: " + _modified.Count );

                OnChildCollectionChanged?.Invoke( this );
            }

        }

        #endregion Event Handlers
    }
}
